Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple policy logic from resource url for getSignedUrlWithCustomPolicy #5862

Merged
merged 10 commits into from
Feb 18, 2025

Conversation

RanVaknin
Copy link
Contributor

@RanVaknin RanVaknin commented Feb 4, 2025

Motivation and Context

Fixes #5577 where users cannot specify a wildcard (*) resource URL policy separate from the actual URL to be signed
Current implementation couples the resource URL for both signing and policy specification
This change allows for more flexible URL signing scenarios.

Modifications

Added new resourceUrlPattern field to CustomSignerRequest
Updated CloudFrontUtilities.getSignedUrlWithCustomPolicy to use resourceUrlPattern when specified

Testing

Added a unit test to verify resourceUrlPattern applies the correct logic to the policy.
Added integration tests to verify real-world scenarios: (Passing on intg test acct ✅ )

  • Testing with query parameters
  • Testing with wildcard paths
  • Testing with full wildcard policy

@RanVaknin RanVaknin requested a review from a team as a code owner February 4, 2025 21:52
@RanVaknin RanVaknin force-pushed the rvaknin/cloudfront-wildcard-policy branch from 9335163 to 95dd934 Compare February 11, 2025 20:45
@@ -331,4 +334,44 @@ void getCookiesForCustomPolicy_withActiveDateAndIpRangeOmitted_producesValidCook
assertThat(cookiesForCustomPolicy.keyPairIdHeaderValue()).isEqualTo("CloudFront-Key-Pair-Id=keyPairId");
}

@Test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a parameterized test to test different possibilities of resourceUrlPattern ?
Something like

@ParameterizedTest
@MethodSource("provideUrlPatternsAndExpectedResources")
void getSignedURLWithCustomPolicy_policyResourceUrlShouldHandleVariousPatterns(
        String resourceUrlPattern, String expectedResource) {
    String baseUrl = "https://d1234.cloudfront.net/images/photo.jpg";
    Instant expiration = Instant.now().plusSeconds(3600);
    CustomSignerRequest request = CustomSignerRequest.builder()
            .resourceUrl(baseUrl)
            .privateKey(keyPair.getPrivate())
            .keyPairId("keyPairId")
            .resourceUrlPattern(resourceUrlPattern)
            .expirationDate(expiration)
            .build();

    SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(request);

    // Extract and decode the policy
    String encodedPolicy = signedUrl.url().split("Policy=")[1].split("&")[0];
    String standardBase64 = encodedPolicy
            .replace('-', '+')
            .replace('_', '=')
            .replace('~', '/');
    String decodedPolicy = new String(Base64.getDecoder().decode(standardBase64), UTF_8);
    
    // Build expected policy
    StringBuilder expectedPolicy = new StringBuilder();
    StringJoiner conditions = new StringJoiner(",", "{", "}");
    conditions.add("\"DateLessThan\":{\"AWS:EpochTime\":" + expiration.getEpochSecond() + "}");
    
    expectedPolicy.append("{\"Statement\": [{")
            .append("\"Resource\":\"").append(expectedResource).append("\",")
            .append("\"Condition\":").append(conditions)
            .append("}]}");

    assertThat(decodedPolicy.trim()).isEqualTo(expectedPolicy.toString().trim());
    
    // Verify URL is properly encoded
    assertThat(signedUrl.url()).doesNotContain(" ");
    assertThat(signedUrl.url()).matches("^[\\x20-\\x7E]+$"); // Printable ASCII characters only
}

private static Stream<Arguments> provideUrlPatternsAndExpectedResources() {
    return Stream.of(
        // Basic patterns
        Arguments.of("*", "*"),
        Arguments.of("https://d1234.cloudfront.net/*", "https://d1234.cloudfront.net/*"),
        Arguments.of("https://d1234.cloudfront.net/images/*", "https://d1234.cloudfront.net/images/*"),
        
        // Specific file patterns
        Arguments.of("234.cloudfront.net/images/photo.jpg", 
                    "https://d1234.cloudfront.net/images/photo.jpg"),
        Arguments.of("https://d1234.cloudfront.net/*/photo.jpg", 
                    "234.cloudfront.net/*/photo.jpg"),
        
        // Multiple wildcards
        Arguments.of("https://*.cloudfront.net/*/photo.jpg", 
                    "https://*.cloudfront.net/*/photo.jpg"),
        
        // Patterns with special characters that need encoding
        Arguments.of("https://d1234.cloudfront.net/images/photo with spaces.jpg", 
                    "https://d1234.cloudfront.net/images/photo%20with%20spaces.jpg"),
        Arguments.of("https://d1234.cloudfront.net/images/photo+with+plus.jpg", 
                    "234.cloudfront.net/images/photo%2Bwith%2Bplus.jpg"),
        Arguments.of("https://d1234.cloudfront.net/images/photo?param=value", 
                    "234.cloudfront.net/images/photo%3Fparam%3Dvalue"),
        
        // Unicode characters
        Arguments.of("https://d1234.cloudfront.net/images/photo-ümlaut.jpg", 
                    "https://d1234.cloudfront.net/images/photo-%C3%BCmlaut.jpg"),
        
        // Path traversal patterns
        Arguments.of("https://d1234.cloudfront.net/images/../photo.jpg", 
                    "https://d1234.cloudfront.net/images/../photo.jpg"),
        
        // Patterns with query parameters
        Arguments.of("https://d1234.cloudfront.net/images/photo.jpg?size=large&format=png", 
                    "https://d1234.cloudfront.net/images/photo.jpg%3Fsize%3Dlarge%26format%3Dpng")
   

Copy link
Contributor Author

@RanVaknin RanVaknin Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameterized test suite is testing for URL encoding transformations that don't exist in the code. In getSignedUrlWithCustomPolicy, the resource pattern is inserted directly into the policy document as a JSON string without modification, and URL-safe encoding (makeStringUrlSafe) happens later to the entire Base64-encoded policy. The test cases expect the Resource field in the policy to contain URL-encoded values (like %20 for spaces), but this is incorrect since the policy is a JSON document, not a URL, and these transformations never occur in the codepath.

For this reason, in this test suite's case, both the Input and Output arguments will be the same every time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Ran
I saw that some how the current implementation on resourceUrl does not handle special cases like below

In CloudFrontUtilitiesIntegrationTest I updated S3_OBJECT_KEY_ON_SUB_PATH to have special charatcters and also updated getSignedUrlWithCustomPolicy_wildCardPath test to cehck for specific url

    private static final String S3_OBJECT_KEY_ON_SUB_PATH = "foo/specific space+plus-üspecial";


    @Test
    void getSignedUrlWithCustomPolicy_wildCardPath() throws Exception {

        System.out.println("bucket® "+bucket);
        String resourceUri = "https://" + domainName;
        Instant expirationDate = LocalDate.of(2050, 1, 1)
                                          .atStartOfDay()
                                          .toInstant(ZoneOffset.of("Z"));

        Instant activeDate = LocalDate.of(2022, 1, 1)
                                      .atStartOfDay()
                                      .toInstant(ZoneOffset.of("Z"));

        CustomSignerRequest request = CustomSignerRequest.builder()
                                                         .resourceUrl(resourceUri + "/foo/specific space+plus-üspecial")
                                                         .privateKey(keyFilePath)
                                                         .keyPairId(keyPairId)
                                                         .policyResource(resourceUri + "/foo/specific space+plus-üspecial")
                                                         .activeDate(activeDate)
                                                         .expirationDate(expirationDate)
                                                         .build();

        SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(request);


        URI modifiedUri = URI.create(signedUrl.url().replace("/specific space+plus-üspecial","/other-file"));
        SdkHttpClient client = ApacheHttpClient.create();
        HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder()
                                                                               .request(SdkHttpRequest.builder()
                                                                                                      .encodedPath(modifiedUri.getRawPath() + "?" + modifiedUri.getRawQuery())
                                                                                                      .host(modifiedUri.getHost())
                                                                                                      .method(SdkHttpMethod.GET)
                                                                                                      .protocol("https")
                                                                                                      .build())
                                                                               .build()).call();
        assertThat(response.httpResponse().statusCode()).isEqualTo(200);
    }

But I get

java.lang.IllegalArgumentException: Illegal character in path at index 50: https://d3j5oja9i0ormb.cloudfront.net/foo/specific space+plus-üspecial


We need not fix this in this PR but raise an internal task and discuss this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I can create a new task on next steps.

For reference, both the JS SDK v3 and Go SDK v2 rely on the standard library's URL parsing, and the provided URLs get properly encoded:
Go SDK impl https://github.com/aws/aws-sdk-go-v2/blob/main/feature/cloudfront/sign/sign_url.go#L194

func main() {
	rawURL := "https://example.cloudfront.net/foo/specific space+plus-üspecial"
	uri, err := url.Parse(rawURL)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Parsed successfully", uri)
	}
}
// Parsed successfully https://example.cloudfront.net/foo/specific%20space+plus-%C3%BCspecial

JS SDK impl https://github.com/aws/aws-sdk-js-v3/blob/main/packages/cloudfront-signer/src/sign.ts#L126

try {
const uri = new URL("https://d3j5oja9i0ormb.cloudfront.net/foo/specific space+plus-üspecial")
    console.log("Parsed ", uri.href)
} catch (e) {
    console.log(e);
}
// "Parsed https://d3j5oja9i0ormb.cloudfront.net/foo/specific%20space+plus-%C3%BCspecial

@RanVaknin RanVaknin added this pull request to the merge queue Feb 18, 2025
String resourceUrl = request.resourceUrl();
String policy = SigningUtils.buildCustomPolicyForSignedUrl(request.resourceUrl(), request.activeDate(),
request.expirationDate(), request.ipRange());
String resourceUrlPattern = StringUtils.isEmpty(request.resourceUrlPattern())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason we are treating empty as null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because resourceUrlPattern maps to the JSON Policy document's Resource field, if a user provides null or "" we default to the resourceUrl itself - meaning, there is no pattern, just use the base url as the Resource field in the policy.

The server side implication of send a policy with a Resource field set to "" will result in a 403. I can't foresee a situation in which users are actively prohibiting all access to their distribution just by passing "".

For example:

        CustomSignerRequest request = CustomSignerRequest.builder()
                                                         .resourceUrl(resourceUrl)
                                                         .privateKey(keyFilePath)
                                                         .keyPairId(keyPairId)
                                                         .resourceUrlPattern("")
                                                         .activeDate(activeDate)
                                                         .expirationDate(expirationDate)
                                                         .build();

Will result in a 403.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, my question is more around: is this something we want to handle in the SDK? Should we just let it fail on the server side instead?

My concern is that some customers may not realize "" is not supported or have bugs in their code to pass empty string to resourceUrlPattern unintentionally, which will be hidden by this SDK logic.

Copy link
Contributor Author

@RanVaknin RanVaknin Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack. I can just use a standard null check.

"type": "bugfix",
"category": "Amazon Cloudfront",
"contributor": "",
"description": "Decouple policy logic from resource url for getSignedUrlWithCustomPolicy"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be implementation detail.

Suggesting:

Allow users to specify resource URL pattern in `CloudFrontUtilities#getSignedUrlWithCustomPolicy`. See [#5577](https://github.com/aws/aws-sdk-java-v2/issues/5577)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Do you want me to raise a PR to amend the changelog?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that'd be great. It may be easier after release is finished.

Merged via the queue into master with commit 5fd2e6e Feb 18, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CloudFrontUtilities : cannot specify wildcard (*) resource url for a custom policy
4 participants